package com.lifeonwalden.fincalc.fund.service;

import com.lifeonwalden.fincalc.fund.bean.FofValuation;
import com.lifeonwalden.fincalc.fund.bean.FundTrade;
import com.lifeonwalden.fincalc.fund.bean.FundValueInfo;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Function;

public class FofValuationProcessor {
    /**
     * @param tradeList      所有子基金的交易记录，按发生日期升序排列
     * @param tradeConverter 转换器：业务交易记录转换为标准交易记录
     * @param fundNavFetcher 子基金净值获取器：根据基金代码和日期，返回该子基金指定日期最新单位净值
     * @param isTradeDate    交易日判断器：返回指定日期是否为交易日，如生成自然日序列的FOF估值，可不提供
     * @param calcEndDate    计算截止日
     * @param <T>
     * @return
     */
    public <T> List<FofValuation> simpleRuleCalc(List<T> tradeList, Function<T, FundTrade> tradeConverter, Function<FundValueInfo, BigDecimal> fundNavFetcher, Optional<Function<Date, Boolean>> isTradeDate, Optional<Date> calcEndDate) {
        Map<String, BigDecimal> fundShareHolder = new HashMap<>();
        List<FofValuation> valuationList = new ArrayList<>();
        Date endDate = new Date();
        if (calcEndDate.isPresent()) {
            endDate = calcEndDate.get();
        }

        /** 第一笔交易 */
        FofValuation valuation = new FofValuation().setNav(BigDecimal.ONE).setAccuNav(BigDecimal.ONE).setShare(BigDecimal.ZERO).setBonus(BigDecimal.ZERO);
        FundTrade trade = tradeConverter.apply(tradeList.get(0));
        switch (trade.getTradeType()) {
            case PURS:
            case SUBR: {
                valuation.setShare(trade.getAmount().setScale(2, RoundingMode.DOWN)).setDate(trade.getDate());
                fundShareHolder.put(trade.getCode(), trade.getShare());
                break;
            }
            default: {
                throw new RuntimeException("Invalid trade records. The trade type of the first trade record has to be PURS or SUBR.");
            }
        }

        /** 处理交易 */
        for (int i = 1; i < tradeList.size(); i++) {
            trade = tradeConverter.apply(tradeList.get(i));

            int dateCompareResult = trade.getDate().compareTo(valuation.getDate());
            if (0 == dateCompareResult) {
                simpleRuleTradeProcessor(trade, valuation, fundShareHolder);
            } else if (dateCompareResult > 0) {
                simpleRuleValuationProcessor(valuation, fundShareHolder, fundNavFetcher);
                valuationList.add(new FofValuation().setDate(valuation.getDate()).setShare(valuation.getShare()).setAccuNav(valuation.getAccuNav()).setNav(valuation.getNav()).setBonus(valuation.getBonus()));

                /** 无交易日期的估值记录填充 */
                Date natureDate = valuation.getDate();
                do {
                    natureDate = Date.from(natureDate.toInstant().plus(1, ChronoUnit.DAYS));
                    int natureDateCompareResult = natureDate.compareTo(trade.getDate());
                    if (0 == natureDateCompareResult) {
                        valuation.setDate(natureDate);
                        simpleRuleTradeProcessor(trade, valuation, fundShareHolder);

                        break;
                    } else if (natureDateCompareResult < 0) {
                        if (false == isTradeDate.isPresent() || isTradeDate.get().apply(natureDate)) {
                            valuation.setDate(natureDate);
                            simpleRuleValuationProcessor(valuation, fundShareHolder, fundNavFetcher);
                            valuationList.add(new FofValuation().setDate(valuation.getDate()).setShare(valuation.getShare()).setAccuNav(valuation.getAccuNav()).setNav(valuation.getNav()).setBonus(valuation.getBonus()));
                        }
                    } else {
                        throw new RuntimeException("Invalid trade records. The records have to be order by trade date asc.");
                    }
                } while (true);
            } else {
                throw new RuntimeException("Invalid trade records. The records have to be order by trade date asc.");
            }
        }

        /** 尾部无交易日期的估值记录填充 */
        do {
            simpleRuleValuationProcessor(valuation, fundShareHolder, fundNavFetcher);
            valuationList.add(new FofValuation().setDate(valuation.getDate()).setShare(valuation.getShare()).setAccuNav(valuation.getAccuNav()).setNav(valuation.getNav()).setBonus(valuation.getBonus()));

            valuation.setDate(Date.from(valuation.getDate().toInstant().plus(1, ChronoUnit.DAYS)));
        } while (valuation.getDate().compareTo(endDate) <= 0);

        return valuationList;
    }

    private void simpleRuleValuationProcessor(FofValuation valuation, Map<String, BigDecimal> fundShareHolder, Function<FundValueInfo, BigDecimal> fundNavFetcher) {
        List<String> emptyFundCodeList = new ArrayList<>();
        BigDecimal newFOFMarketValue = BigDecimal.ZERO;
        for (String code : fundShareHolder.keySet()) {
            BigDecimal share = fundShareHolder.get(code);
            if (BigDecimal.ZERO.compareTo(share) == 0) {
                emptyFundCodeList.add(code);
            } else {
                newFOFMarketValue = newFOFMarketValue.add(share.multiply(fundNavFetcher.apply(new FundValueInfo().setCode(code).setDate(valuation.getDate()))).setScale(2, RoundingMode.DOWN));
            }
        }

        emptyFundCodeList.forEach(code -> fundShareHolder.remove(code));

        valuation.setNav(newFOFMarketValue.divide(valuation.getShare(), 4, RoundingMode.DOWN)).setAccuNav(newFOFMarketValue.add(valuation.getBonus()).divide(valuation.getShare(), 4, RoundingMode.DOWN));
    }

    private void simpleRuleTradeProcessor(FundTrade trade, FofValuation valuation, Map<String, BigDecimal> fundShareHolder) {
        switch (trade.getTradeType()) {
            case SUBR:
            case PURS: {
                valuation.setShare(valuation.getShare().add(trade.getAmount().divide(valuation.getNav(), 2, RoundingMode.DOWN)));

                BigDecimal share = fundShareHolder.get(trade.getCode());
                if (null != share) {
                    fundShareHolder.put(trade.getCode(), share.add(trade.getShare()));
                } else {
                    fundShareHolder.put(trade.getCode(), trade.getShare());
                }

                break;
            }
            case REDEEM: {
                BigDecimal fofChangeShare = trade.getAmount().divide(valuation.getNav(), 2, RoundingMode.DOWN);
                if (fofChangeShare.compareTo(valuation.getShare()) <= 0) {
                    valuation.setShare(valuation.getShare().subtract(fofChangeShare));
                } else {
                    throw new RuntimeException("Invalid redeem trade amount for ".concat(trade.getCode()).concat("on ").concat(trade.getDate().toString()));
                }

                BigDecimal share = fundShareHolder.get(trade.getCode());
                if (null != share && share.compareTo(trade.getShare()) >= 0) {
                    fundShareHolder.put(trade.getCode(), share.subtract(trade.getShare()));
                } else {
                    throw new RuntimeException("Invalid redeem trade share for ".concat(trade.getCode()).concat("on ").concat(trade.getDate().toString()));
                }

                break;
            }
            case BONUS: {
                if (fundShareHolder.containsKey(trade.getCode())) {
                    valuation.setBonus(valuation.getBonus().add(trade.getAmount()));
                } else {
                    throw new RuntimeException("Invalid bonus trade for ".concat(trade.getCode()).concat("on ").concat(trade.getDate().toString()));
                }
                break;
            }
            case REWARD:
            case SHARE_DEDUCTION: {
                BigDecimal share = fundShareHolder.get(trade.getCode());
                if (null != share && share.compareTo(trade.getShare()) >= 0) {
                    fundShareHolder.put(trade.getCode(), share.subtract(trade.getShare()));
                } else {
                    throw new RuntimeException("Invalid ".concat(trade.getTradeType().name()).concat(" trade for ".concat(trade.getCode()).concat("on ").concat(trade.getDate().toString())));
                }
                break;
            }
            case BONUS_SHARE:
            case SHARE_RAISE: {
                BigDecimal share = fundShareHolder.get(trade.getCode());
                if (null != share) {
                    fundShareHolder.put(trade.getCode(), share.add(trade.getShare()));
                } else {
                    throw new RuntimeException("Invalid ".concat(trade.getTradeType().name()).concat(" trade for ".concat(trade.getCode()).concat("on ").concat(trade.getDate().toString())));
                }
                break;
            }
        }
    }
}
